دکوراتورهای جاوااسکریپت: قابلیت قدرتمند فرابرنامهنویسی برای افزودن فراداده و پیادهسازی AOP. با مثالهای عملی، قابلیت استفاده مجدد، خوانایی و نگهداری کد را بهبود بخشید.
دکوراتورهای جاوااسکریپت: برنامهنویسی فراداده و الگوهای AOP
دکوراتورهای جاوااسکریپت یک قابلیت قدرتمند و رسا در فرابرنامهنویسی هستند که به شما امکان میدهند رفتار کلاسها، متدها، پراپرتیها و پارامترها را به روشی اعلانی و قابل استفاده مجدد تغییر دهید یا بهبود بخشید. آنها یک سینتکس مختصر برای افزودن فراداده و پیادهسازی اصول برنامهنویسی جنبهگرا (AOP) فراهم میکنند که قابلیت استفاده مجدد، خوانایی و نگهداری کد را بهبود میبخشد. این راهنمای جامع، دکوراتورهای جاوااسکریپت را به تفصیل بررسی میکند و سینتکس، کاربرد و برنامههای آنها را در سناریوهای مختلف پوشش میدهد. در حالی که هنوز رسماً یک پیشنهاد در حال تکامل هستند، دکوراتورها به طور گستردهای پذیرفته شدهاند، به خصوص در فریمورکهایی مانند Angular و NestJS، و تأثیر آنها بر توسعه جاوااسکریپت غیرقابل انکار است.
دکوراتورهای جاوااسکریپت چه هستند؟
دکوراتورها نوع خاصی از اعلان هستند که میتوانند به اعلان کلاس، متد، اکسسور، پراپرتی یا پارامتر متصل شوند. آنها از فرم @expression استفاده میکنند، که در آن expression باید به تابعی ارزیابی شود که در زمان اجرا با اطلاعات مربوط به اعلان تزئین شده فراخوانی خواهد شد. اساساً، دکوراتورها به عنوان توابعی عمل میکنند که عنصر تزئین شده را دربر میگیرند یا تغییر میدهند و به شما امکان میدهند بدون تغییر مستقیم کد اصلی، قابلیت اضافی یا فراداده را اضافه کنید.
دکوراتورها را به عنوان حاشیهنویسیها یا نشانگرهایی در نظر بگیرید که میتوانند به عناصر کد متصل شوند. سپس این نشانگرها میتوانند در زمان اجرا پردازش شوند تا وظایف مختلفی مانند ثبت وقایع (logging)، اعتبارسنجی (validation)، احراز هویت (authorization) یا تزریق وابستگی (dependency injection) را انجام دهند. دکوراتورها با جداسازی نگرانیها و کاهش کدهای تکراری، ساختار کد تمیزتر و ماژولارتری را ترویج میکنند.
مزایای استفاده از دکوراتورها
- بهبود قابلیت استفاده مجدد کد: دکوراتورها به شما امکان میدهند رفتارهای رایج را در کامپوننتهای قابل استفاده مجدد کپسوله کنید که میتوانند در چندین بخش از برنامه شما اعمال شوند. این امر تکرار کد را کاهش میدهد و سازگاری را ترویج میکند.
- افزایش خوانایی: با جداسازی نگرانیهای متقاطع در دکوراتورها، میتوانید منطق اصلی خود را تمیزتر و قابل فهمتر کنید. دکوراتورها راهی اعلانی برای بیان رفتارهای اضافی فراهم میکنند و کد را خودتوضیحدهنده میسازند.
- افزایش قابلیت نگهداری: دکوراتورها ماژولار بودن و جداسازی نگرانیها را ترویج میکنند و اصلاح یا گسترش برنامه شما را بدون تأثیر بر سایر بخشهای پایگاه کد آسانتر میسازند. این امر خطر معرفی اشکالات را کاهش میدهد و فرآیند نگهداری را سادهتر میکند.
- برنامهنویسی جنبهگرا (AOP): دکوراتورها شما را قادر میسازند تا اصول AOP را با اجازه دادن به شما برای تزریق رفتار به کدهای موجود بدون تغییر کد منبع آن، پیادهسازی کنید. این امر به ویژه برای مدیریت نگرانیهای متقاطع مانند ثبت وقایع، امنیت و مدیریت تراکنشها مفید است.
انواع دکوراتورها
دکوراتورهای جاوااسکریپت را میتوان به انواع مختلفی از اعلانها اعمال کرد که هر کدام هدف و سینتکس خاص خود را دارند:
دکوراتورهای کلاس
دکوراتورهای کلاس به سازنده کلاس اعمال میشوند و میتوانند برای تغییر تعریف کلاس یا افزودن فراداده استفاده شوند. یک دکوراتور کلاس، سازنده کلاس را به عنوان تنها آرگومان خود دریافت میکند.
مثال: افزودن فراداده به یک کلاس.
function Component(options: { selector: string, template: string }) {
return function <T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
selector = options.selector;
template = options.template;
}
}
}
@Component({ selector: 'my-component', template: '<div>Hello</div>' })
class MyComponent {
constructor() {
// ...
}
}
console.log(new MyComponent().selector); // Output: my-component
در این مثال، دکوراتور Component پراپرتیهای selector و template را به کلاس MyComponent اضافه میکند و به شما امکان میدهد فراداده کامپوننت را به روشی اعلانی پیکربندی کنید. این شبیه به نحوه تعریف کامپوننتهای Angular است.
دکوراتورهای متد
دکوراتورهای متد به متدهای درون یک کلاس اعمال میشوند و میتوانند برای تغییر رفتار متد یا افزودن فراداده استفاده شوند. یک دکوراتور متد سه آرگومان دریافت میکند:
- آبجکت هدف (یا پروتوتایپ کلاس یا سازنده کلاس، بسته به اینکه متد static باشد).
- نام متد.
- توصیفگر پراپرتی برای متد.
مثال: ثبت فراخوانیهای متد.
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned: ${result}`);
return result;
}
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number) {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Output: Calling add with arguments: [2,3]
// add returned: 5
در این مثال، دکوراتور Log فراخوانی متد و آرگومانهای آن را قبل از اجرای متد اصلی ثبت میکند و مقدار بازگشتی را پس از اجرا ثبت میکند. این یک مثال ساده از نحوه استفاده از دکوراتورها برای پیادهسازی قابلیت ثبت وقایع یا ممیزی بدون تغییر منطق اصلی متد است.
دکوراتورهای پراپرتی
دکوراتورهای پراپرتی به پراپرتیهای درون یک کلاس اعمال میشوند و میتوانند برای تغییر رفتار پراپرتی یا افزودن فراداده استفاده شوند. یک دکوراتور پراپرتی دو آرگومان دریافت میکند:
- آبجکت هدف (یا پروتوتایپ کلاس یا سازنده کلاس، بسته به اینکه پراپرتی static باشد).
- نام پراپرتی.
مثال: اعتبارسنجی مقادیر پراپرتی.
function Validate(target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
if (typeof newVal !== 'number' || newVal < 0) {
throw new Error(`Invalid value for ${propertyKey}. Must be a non-negative number.`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Product {
@Validate
price: number;
constructor(price: number) {
this.price = price;
}
}
const product = new Product(10);
console.log(product.price); // Output: 10
try {
product.price = -5; // Throws an error
} catch (e) {
console.error(e.message);
}
در این مثال، دکوراتور Validate پراپرتی price را اعتبارسنجی میکند تا اطمینان حاصل شود که یک عدد غیرمنفی است. اگر یک مقدار نامعتبر اختصاص داده شود، یک خطا پرتاب میشود. این یک مثال ساده از نحوه استفاده از دکوراتورها برای پیادهسازی اعتبارسنجی داده است.
دکوراتورهای پارامتر
دکوراتورهای پارامتر به پارامترهای یک متد اعمال میشوند و میتوانند برای افزودن فراداده یا تغییر رفتار پارامتر استفاده شوند. یک دکوراتور پارامتر سه آرگومان دریافت میکند:
- آبجکت هدف (یا پروتوتایپ کلاس یا سازنده کلاس، بسته به اینکه متد static باشد).
- نام متد.
- شاخص پارامتر در لیست پارامترهای متد.
مثال: تزریق وابستگیها.
import 'reflect-metadata';
const Injectable = (): ClassDecorator => {
return (target: any) => {
Reflect.defineMetadata('injectable', true, target);
};
};
const Inject = (token: string): ParameterDecorator => {
return (target: any, propertyKey: string | symbol, parameterIndex: number) => {
let existingParameters: string[] = Reflect.getOwnMetadata('parameters', target, propertyKey) || [];
existingParameters[parameterIndex] = token;
Reflect.defineMetadata('parameters', existingParameters, target, propertyKey);
};
};
@Injectable()
class Logger {
log(message: string) {
console.log(`Logger: ${message}`);
}
}
class Greeter {
private logger: Logger;
constructor(@Inject('Logger') logger: Logger) {
this.logger = logger;
}
greet(name: string) {
this.logger.log(`Hello, ${name}!`);
}
}
// Simple dependency injection container
class Container {
private dependencies: Map<string, any> = new Map();
register(token: string, dependency: any) {
this.dependencies.set(token, dependency);
}
resolve<T>(target: any): T {
const parameters: string[] = Reflect.getMetadata('parameters', target) || [];
const resolvedDependencies = parameters.map(token => this.dependencies.get(token));
return new target(...resolvedDependencies);
}
}
const container = new Container();
container.register('Logger', new Logger());
const greeter = container.resolve<Greeter>(Greeter);
greeter.greet('World'); // Output: Logger: Hello, World!
در این مثال، دکوراتور Inject برای تزریق وابستگیها به سازنده کلاس Greeter استفاده میشود. این دکوراتور یک توکن را با پارامتر مرتبط میکند که سپس میتواند برای حل وابستگی با استفاده از یک کانتینر تزریق وابستگی استفاده شود. این مثال یک پیادهسازی اساسی از تزریق وابستگی با استفاده از دکوراتورها و کتابخانه reflect-metadata را نشان میدهد.
مثالهای عملی و موارد استفاده
دکوراتورهای جاوااسکریپت را میتوان در سناریوهای مختلفی برای بهبود کیفیت کد و سادهسازی توسعه استفاده کرد. در اینجا چند مثال عملی و مورد استفاده آورده شده است:
ثبت وقایع و ممیزی
دکوراتورها میتوانند برای ثبت خودکار فراخوانیهای متد، آرگومانها و مقادیر بازگشتی استفاده شوند و بینشهای ارزشمندی در مورد رفتار و عملکرد برنامه ارائه دهند. این امر به ویژه برای اشکالزدایی و عیبیابی مسائل مفید است.
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const startTime = performance.now();
console.log(`[${new Date().toISOString()}] Calling method: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
const endTime = performance.now();
const executionTime = endTime - startTime;
console.log(`[${new Date().toISOString()}] Method ${propertyKey} returned: ${result}. Execution time: ${executionTime.toFixed(2)}ms`);
return result;
};
return descriptor;
}
class ExampleClass {
@LogMethod
complexOperation(a: number, b: number): number {
// Simulate a time-consuming operation
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += a + b + i;
}
return sum;
}
}
const example = new ExampleClass();
example.complexOperation(5, 10);
این مثال گسترشیافته زمان اجرای متد را اندازهگیری کرده و آن را به همراه برچسب زمانی فعلی ثبت میکند، که اطلاعات دقیقتری برای تحلیل عملکرد فراهم میآورد.
احراز هویت و مجوزدهی
دکوراتورها میتوانند برای اعمال سیاستهای امنیتی با بررسی نقشها و مجوزهای کاربر قبل از اجرای یک متد استفاده شوند. این امر میتواند از دسترسی غیرمجاز به دادهها و قابلیتهای حساس جلوگیری کند.
function Authorize(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const userRole = getCurrentUserRole(); // Function to retrieve the current user's role
if (userRole !== role) {
throw new Error(`Unauthorized: User does not have the required role (${role}) to access this method.`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
function getCurrentUserRole(): string {
// In a real application, this would retrieve the user's role from authentication context
return 'admin'; // Example: Hardcoded role for demonstration
}
class AdminPanel {
@Authorize('admin')
deleteUser(userId: number) {
console.log(`User ${userId} deleted successfully.`);
}
@Authorize('editor')
editArticle(articleId: number) {
console.log(`Article ${articleId} edited successfully.`);
}
}
const adminPanel = new AdminPanel();
try {
adminPanel.deleteUser(123);
adminPanel.editArticle(456); // This will throw an error because the user role is 'admin'
} catch (error) {
console.error(error.message);
}
در این مثال گسترشیافته، دکوراتور Authorize قبل از اجازه دسترسی به متد، بررسی میکند که آیا کاربر فعلی دارای نقش مشخص شده است یا خیر. تابع getCurrentUserRole (که در یک برنامه واقعی نقش واقعی کاربر را واکشی میکند) برای تعیین نقش فعلی کاربر استفاده میشود. اگر کاربر نقش مورد نیاز را نداشته باشد، یک خطا پرتاب میشود که از اجرای متد جلوگیری میکند.
کشینگ (ذخیرهسازی موقت)
دکوراتورها میتوانند برای کش کردن نتایج عملیاتهای گرانقیمت استفاده شوند، که عملکرد برنامه را بهبود بخشیده و بار سرور را کاهش میدهد. این امر به ویژه برای دادههایی که مکرراً دسترسی پیدا میکنند و اغلب تغییر نمیکنند، مفید است.
function Cache(ttl: number = 60) { // ttl in seconds, default to 60 seconds
const cache = new Map();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cacheKey = `${propertyKey}-${JSON.stringify(args)}`;
const cachedData = cache.get(cacheKey);
if (cachedData && Date.now() < cachedData.expiry) {
console.log(`Retrieving from cache: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
return cachedData.data;
}
console.log(`Executing and caching: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, {
data: result,
expiry: Date.now() + ttl * 1000, // Calculate expiry time
});
return result;
};
return descriptor;
};
}
class DataService {
@Cache(120) // Cache for 120 seconds
async fetchData(id: number): Promise<string> {
// Simulate fetching data from a database or API
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Data for ID ${id} fetched from source.`);
}, 1000); // Simulate a 1-second delay
});
}
}
const dataService = new DataService();
(async () => {
console.log(await dataService.fetchData(1)); // Executes the method
console.log(await dataService.fetchData(1)); // Retrieves from cache
await new Promise(resolve => setTimeout(resolve, 121000)); // Wait for 121 seconds to allow the cache to expire
console.log(await dataService.fetchData(1)); // Executes the method again after cache expiry
})();
این مثال گسترشیافته یک مکانیسم کشینگ اولیه را با استفاده از یک Map پیادهسازی میکند. دکوراتور Cache نتایج متد تزئین شده را برای یک زمان حیات (TTL) مشخص ذخیره میکند. هنگامی که متد دوباره با همان آرگومانها فراخوانی میشود، نتیجه کش شده به جای اجرای مجدد متد بازگردانده میشود. پس از انقضای TTL، متد دوباره اجرا میشود و نتیجه کش میشود.
اعتبارسنجی
دکوراتورها میتوانند برای اعتبارسنجی دادهها قبل از پردازش آنها استفاده شوند، که یکپارچگی دادهها را تضمین کرده و از بروز خطاها جلوگیری میکند. این امر به ویژه برای اعتبارسنجی ورودی کاربر یا دادههای دریافتی از منابع خارجی مفید است.
function Required() {
return function (target: any, propertyKey: string) {
if (!target.constructor.requiredFields) {
target.constructor.requiredFields = [];
}
target.constructor.requiredFields.push(propertyKey);
};
}
function ValidateClass(target: any) {
const originalConstructor = target;
function construct(constructor: any, args: any[]) {
const instance: any = new constructor(...args);
if (constructor.requiredFields) {
constructor.requiredFields.forEach((field: string) => {
if (!instance[field]) {
throw new Error(`Missing required field: ${field}`);
}
});
}
return instance;
}
const newConstructor: any = function (...args: any[]) {
return construct(originalConstructor, args);
};
newConstructor.prototype = originalConstructor.prototype;
return newConstructor;
}
@ValidateClass
class User {
@Required()
name: string;
@Required()
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
try {
const validUser = new User('John Doe', 'john.doe@example.com');
console.log('Valid user created:', validUser);
const invalidUser = new User('Jane Doe', ''); // Missing email
} catch (error) {
console.error('Validation error:', error.message);
}
این مثال از دو دکوراتور استفاده میکند: Required و ValidateClass. دکوراتور Required پراپرتیها را به عنوان الزامی علامتگذاری میکند. دکوراتور ValidateClass سازنده کلاس را رهگیری کرده و بررسی میکند که آیا تمام فیلدهای الزامی دارای مقادیر هستند یا خیر. اگر هر فیلد الزامی از دست رفته باشد، یک خطا پرتاب میشود.
تزریق وابستگی
همانطور که در مثال دکوراتور پارامتر نشان داده شد، دکوراتورها میتوانند تزریق وابستگی اولیه را تسهیل کنند و مدیریت وابستگیها و جداسازی کامپوننتها را آسانتر سازند. در حالی که فریمورکهای پیچیدهتری برای تزریق وابستگی وجود دارند، دکوراتورها میتوانند راهی سبکوزن و مناسب برای مدیریت سناریوهای ساده تزریق وابستگی فراهم کنند.
ملاحظات و بهترین شیوهها
- درک زمینه اجرا: از آرگومانهای
target،propertyKeyوdescriptorکه به تابع دکوراتور ارسال میشوند، آگاه باشید. این آرگومانها اطلاعات ارزشمندی در مورد اعلان تزئین شده فراهم میکنند و به شما امکان میدهند رفتار آن را بر این اساس تغییر دهید. - استفاده محتاطانه از دکوراتورها: در حالی که دکوراتورها میتوانند قدرتمند باشند، استفاده بیش از حد میتواند منجر به کدی پیچیده و دشوار برای درک شود. از دکوراتورها با احتیاط و فقط زمانی استفاده کنید که مزایای واضحی از نظر قابلیت استفاده مجدد، خوانایی یا قابلیت نگهداری کد ارائه میدهند.
- رعایت قراردادهای نامگذاری: از نامهای توصیفی برای دکوراتورهای خود استفاده کنید تا هدف آنها را به وضوح نشان دهید. این کار کد شما را خودتوضیحدهنده و قابل فهمتر میکند.
- حفظ جداسازی نگرانیها: دکوراتورها باید بر نگرانیهای متقاطع خاص تمرکز کنند و از ترکیب قابلیتهای نامرتبط اجتناب ورزند. این امر ماژولار بودن و قابلیت نگهداری کد شما را بهبود میبخشد.
- تست دقیق دکوراتورهای خود: مانند هر کد دیگری، دکوراتورها باید به طور کامل تست شوند تا اطمینان حاصل شود که به درستی کار میکنند و عوارض جانبی ناخواسته ایجاد نمیکنند.
- مراقب عوارض جانبی باشید: دکوراتورها در زمان اجرا اجرا میشوند. از عملیاتهای پیچیده یا طولانیمدت در توابع دکوراتور خودداری کنید، زیرا این میتواند بر عملکرد برنامه تأثیر بگذارد.
- تایپاسکریپت توصیه میشود: در حالی که دکوراتورهای جاوااسکریپت از لحاظ فنی میتوانند در جاوااسکریپت معمولی با ترانسپایل Babel استفاده شوند، اما معمولاً با تایپاسکریپت به کار میروند. تایپاسکریپت ایمنی نوع عالی و بررسیهای زمان طراحی را برای دکوراتورها فراهم میکند.
دیدگاهها و مثالهای جهانی
اصول قابلیت استفاده مجدد کد، قابلیت نگهداری و جداسازی نگرانیها که دکوراتورها تسهیل میکنند، به طور جهانی در زمینههای مختلف توسعه نرمافزار قابل اجرا هستند. با این حال، پیادهسازیها و موارد استفاده خاص ممکن است بسته به پشته فناوری، الزامات پروژه و شیوههای توسعه رایج در مناطق مختلف متفاوت باشد.
به عنوان مثال، در توسعه جاوا سازمانی، حاشیهنویسیها (از نظر مفهومی شبیه به دکوراتورها) به طور گستردهای برای پیکربندی و تزریق وابستگی (مانند Spring Framework) استفاده میشوند. در حالی که سینتکس و مکانیزمهای زیربنایی با دکوراتورهای جاوااسکریپت متفاوت است، اصول زیربنایی فرابرنامهنویسی و AOP یکسان باقی میمانند. به طور مشابه، در پایتون، دکوراتورها یک قابلیت زبان درجه یک هستند و مکرراً برای وظایفی مانند ثبت وقایع، احراز هویت و کشینگ استفاده میشوند.
هنگام کار در تیمهای بینالمللی یا مشارکت در پروژههای منبع باز با مخاطبان جهانی، رعایت استانداردها و بهترین شیوههای کدنویسی که وضوح و قابلیت نگهداری را ترویج میکنند، ضروری است. استفاده موثر از دکوراتورها میتواند به یک پایگاه کد ماژولارتر و با ساختار بهتر کمک کند و همکاری و مشارکت توسعهدهندگان با پیشزمینههای مختلف را آسانتر سازد.
نتیجهگیری
دکوراتورهای جاوااسکریپت یک قابلیت قدرتمند و چندمنظوره فرابرنامهنویسی هستند که میتوانند به طور قابل توجهی قابلیت استفاده مجدد، خوانایی و قابلیت نگهداری کد را بهبود بخشند. با فراهم کردن راهی اعلانی برای افزودن فراداده و پیادهسازی اصول AOP، دکوراتورها شما را قادر میسازند تا رفتارهای رایج را کپسوله کنید، نگرانیها را جدا کنید و برنامههایی ماژولارتر و با ساختار بهتر ایجاد کنید. در حالی که هنوز یک پیشنهاد در حال توسعه فعال هستند، دکوراتورها قبلاً پذیرش گستردهای در فریمورکهایی مانند Angular و NestJS پیدا کردهاند و آماده تبدیل شدن به بخش مهمی از اکوسیستم جاوااسکریپت هستند. با درک سینتکس، کاربرد و بهترین شیوههای دکوراتورها، میتوانید از قدرت آنها برای ساخت برنامههایی قویتر، مقیاسپذیرتر و قابل نگهداریتر استفاده کنید.
همچنان که اکوسیستم جاوااسکریپت به تکامل خود ادامه میدهد، آگاه ماندن از قابلیتهای جدید و بهترین شیوهها برای ساخت نرمافزار با کیفیت بالا که نیازهای کاربران در سراسر جهان را برآورده میکند، بسیار حیاتی است. تسلط بر دکوراتورهای جاوااسکریپت یک مهارت ارزشمند است که میتواند به شما کمک کند تا توسعهدهندهای مؤثرتر و پربارتر شوید.